using iTextSharp.text.exceptions;
using iTextSharp.text.pdf;
using iTextSharp.text.pdf.parser;
using Pdf2epub.Interfaces;
using Pdf2epub.Model;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
using ContentType = Pdf2epub.ExtractStrategy.ContentType;
using LineSegment = iTextSharp.text.pdf.parser.LineSegment;
using Matrix = iTextSharp.text.pdf.parser.Matrix;
using Path = System.IO.Path;

namespace Pdf2epub.BusinessLogic.Extractors.ExtractStrategy
{
    public enum OperatingMode
    {
        Extract,
        Analyze
    }

    public class HtmlTextExtractionStrategy : ITextExtractionStrategy
    {
        private readonly List<ContentBlock> m_ContentBlocks = new List<ContentBlock>();
        private readonly FontUsageCollection m_FontUsages = new FontUsageCollection();
        private readonly List<IPdfElementChunk> m_FoundChunks = new List<IPdfElementChunk>();

        private readonly List<float> m_LineDistances = new List<float>();

        private readonly IProjectContext m_ProjectContext;
        private ContentBlock m_ContentBlock;
        private bool m_ContentBlockSorted;
        private int m_ImageNumber;
        private IPdfElementChunk m_LastChunk;
        private List<float> m_LineStarts;
        private Dictionary<float, int> m_PossibleColumns;

        public HtmlTextExtractionStrategy(IProjectContext projectContext)
        {
            m_ProjectContext = projectContext;
            ImagePath = string.Empty;
        }

        public double AverageLineDistance
        {
            get
            {
                return m_LineDistances.Average();
            }
        }

        public ContentBlock[] ContentBlocks
        {
            get
            {
                if (!m_ContentBlockSorted)
                {
                    //m_ContentBlocks.Sort(SortContenBlockByPosition);
                    m_ContentBlockSorted = true;
                }
                return m_ContentBlocks.ToArray();
            }
        }

        //private static int SortContenBlockByPosition(ContentBlock block1, ContentBlock block2)
        //{
        //    if (block1.FirstChunkBaseLine == block2.FirstChunkBaseLine)
        //    {
        //        return block1.OriginalXPosition.CompareTo(block2.OriginalXPosition);
        //    }
        //    return block2.FirstChunkBaseLine.CompareTo(block1.FirstChunkBaseLine) * 1000000;
        //}

        public OperatingMode CurrentOperatingMode { get; set; }

        public FontUsageCollection FontUsages
        {
            get { return m_FontUsages; }
        }

        public string HtmlPath { get; set; }

        public string ImagePath { get; set; }

        public double MaximumLineDistance
        {
            get
            {
                return m_LineDistances.Max();
            }
        }

        public double MinimumLineDistance
        {
            get
            {
                return m_LineDistances.Min();
            }
        }

        public double MostlyUsedLineDistance
        {
            get
            {
                Dictionary<float, int> used = new Dictionary<float, int>();

                foreach (float dist in m_LineDistances)
                {
                    if (!used.ContainsKey(dist))
                        used[dist] = 1;
                    else
                        used[dist]++;
                }

                float maxUsed = 0;
                float maxCount = 0;
                foreach (float dist in used.Keys.Where(dist => used[dist] > maxCount))
                {
                    maxCount = used[dist];
                    maxUsed = dist;
                }

                return maxUsed;
            }
        }

        public float[] PossibleColumns
        {
            get
            {
                List<float> columns = m_PossibleColumns.Where(column => column.Value > 1).Select(column => column.Key).ToList();
                columns.Sort();
                return columns.ToArray();
            }
        }

        public bool PreviewMode { get; set; }

        public bool TakeFirstPictureAsCover { get; set; }

        private float GetAverageFirstXPosInLine
        {
            get
            {
                if (m_LineStarts == null)
                {
                    m_LineStarts = new List<float>();
                    foreach (var textChunk in m_FoundChunks.Where(c => c.GetType() == typeof(TextChunk)))
                    {
                        float min = m_FoundChunks.Where(t => Math.Abs(t.YPos - textChunk.YPos) < m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset).Min(t => t.XPos);
                        m_LineStarts.Add(min);
                    }
                }
                return m_LineStarts.Average();
            }
        }

        public virtual void BeginTextBlock()
        {
        }

        public void Clear()
        {
            m_LineStarts = null;
            m_ContentBlocks.Clear();
            m_FontUsages.Clear();
            m_LineDistances.Clear();
            m_FoundChunks.Clear();
        }

        public void ClearContentBlocks()
        {
            m_ContentBlockSorted = false;
            m_LineStarts = null;
            m_ContentBlocks.Clear();
        }

        public void ClearImageCounter()
        {
            m_ImageNumber = 0;
        }

        public virtual void EndTextBlock()
        {
        }

        public virtual String GetResultantText()
        {
            RemoveTextchunksOutsideDetectionRange();
            RemoveChunksWithRemoveStyle();

            m_FoundChunks.Sort(SortPdfChunks);

            float[] lines = GetLines();
            List<float> paragraphStartLines = DetectParagraphs(lines);

            CalculatePossibleColumnFromTextChunks();

            List<TableRange> detectedTables = DetectTablesOnPage(lines);

            StringBuilder sb = new StringBuilder();

            m_ContentBlock = null;
            m_LastChunk = null;

            for (int lineNr = 0; lineNr < lines.Length; lineNr++)
            {
                float currentLine = lines[lineNr];
                IPdfElementChunk[] chunksInCurrentLine = GetChunksForLine(currentLine);

                bool tableHandled = false;

                if ((m_ProjectContext.ProjectConfiguration.DetectTables) && (CurrentOperatingMode == OperatingMode.Extract)&& (IsInTable(detectedTables, currentLine)))
                {
                    if ((m_ContentBlock != null) && (m_ContentBlock.ContentType != ContentType.Table))
                    {
                        InsertContentBlock(m_ContentBlock);
                        m_ContentBlock = null;
                    }

                    if (m_ContentBlock == null)
                    {
                        CreateNewTableBlock(detectedTables, currentLine);
                    }

                    tableHandled = true;
                    
                    for(int c=0; c<chunksInCurrentLine.Length; c++)
                    {
                        IPdfElementChunk chunk = chunksInCurrentLine[c];

                        ContentBlock cellBlock = FindTableCellForChunk(detectedTables, m_ContentBlock, chunk);
                        if (cellBlock != null)
                        {
                            if (chunk is TextChunk)
                            {
                                TextChunk textChunk = chunk as TextChunk;
                                if (cellBlock.Font == null)
                                {
                                    cellBlock.Font = GetFontUsage(textChunk.Font, textChunk.FontSize);
                                }

                                if (c > 0)
                                {
                                    TextChunk lastTextChunk = chunksInCurrentLine[c - 1] as TextChunk;
                                    AppendSpaceIfRequired(textChunk, lastTextChunk, cellBlock);
                                }
                                cellBlock.Append(textChunk.Text);
                            }
                        }
                        else
                        {
                            tableHandled = false;
                            break;
                        }
                    }
                }

                if (!tableHandled)
                {
                    foreach (IPdfElementChunk chunk in chunksInCurrentLine)
                    {
                        bool newContentBlockRequired = NewContentBlockRequired(chunk);
                        bool isNewParapgraph = IsNewParagraph(paragraphStartLines, chunk.YPos);
                        bool isIndent = IsIndent(chunk, chunksInCurrentLine);

                        if (newContentBlockRequired || isNewParapgraph || isIndent)
                        {
                            if (m_ContentBlock != null)
                            {
                                InsertContentBlock(m_ContentBlock);
                            }
                            m_ContentBlock = null;
                        }

                        if (chunk is TextChunk)
                        {
                            TextChunk textChunk = chunk as TextChunk;

                            if (!PositionIsInSelectedRange(textChunk))
                                continue;

                            if (!FontSizeIsInSelectedRange(textChunk))
                                continue;

                            if (m_ContentBlock == null)
                            {
                                CreateNewTextBlock(textChunk);
                            }

                            if (m_ContentBlock != null)
                            {
                                AppendTextToCurrentBlock(textChunk);
                            }
                        }
                        else if (chunk is ImageChunk)
                        {
                            if (m_ContentBlock != null)
                            {
                                InsertContentBlock(m_ContentBlock);
                            }
                            m_ContentBlock = ProcessImageChunk(chunk as ImageChunk);
                        }
                        m_LastChunk = chunk;
                    }
                }
            }

            if (m_ContentBlock != null)
            {
                InsertContentBlock(m_ContentBlock);
            }

            m_FoundChunks.Clear();

            return sb.ToString();
        }

        
        private static void AppendSpaceIfRequired(TextChunk textChunk, TextChunk lastTextChunk, ContentBlock block)
        {
            if (lastTextChunk != null)
            {
                if (!string.IsNullOrEmpty(block.Content) &&
                    (Math.Abs(textChunk.XPos - lastTextChunk.distParallelEnd) > (textChunk.CharSpaceWidth/2)))
                {
                    block.Append(" ");
                }
            }
        }

        public virtual void RenderImage(ImageRenderInfo renderInfo)
        {
            if (CurrentOperatingMode != OperatingMode.Extract)
                return;

            if (!Directory.Exists(ImagePath))
                Directory.CreateDirectory(ImagePath);

            string imageName = string.Format("img_{0:X8}", m_ImageNumber);
            if (TakeFirstPictureAsCover && m_ProjectContext.ProjectConfiguration.FirstImageElementIsCoverImage && (m_ImageNumber == 0))
            {
                imageName = "cover";
            }
            m_ImageNumber++;

            try
            {
                PdfImageObject pdfImageObject = renderInfo.GetImage();

                string imagePath = string.Format("{0}.jpg", Path.Combine(ImagePath, imageName));
                Image image = pdfImageObject.GetDrawingImage();
                if (IsCmykImage(pdfImageObject))
                {
                    // TODO: Know issue: CMYK images in PDF will be interpreted as RBG by .Net
                    // Somehow this must be converted...
                }

                if ((image.Height < m_ProjectContext.ProjectConfiguration.IgnoreImagesSmallThanPixel) ||
                    (image.Width < m_ProjectContext.ProjectConfiguration.IgnoreImagesSmallThanPixel))
                {
                    return;
                }

                if ((imageName == "cover") && (m_ProjectContext.ProjectConfiguration.MinCoverImageSize > 0))
                {
                    if ((image.Width < m_ProjectContext.ProjectConfiguration.MinCoverImageSize) &&
                        (image.Height < m_ProjectContext.ProjectConfiguration.MinCoverImageSize))
                    {
                        image = ResizeImage(image, m_ProjectContext.ProjectConfiguration.MinCoverImageSize, false);
                    }
                }

                if (m_ProjectContext.ProjectConfiguration.ImageProcessing == ImageProcessing.ShrinkToMaxSize)
                {
                    image = ResizeImage(image, m_ProjectContext.ProjectConfiguration.MaxImageSize, true);
                }

                image.Save(imagePath, ImageFormat.Jpeg);

                Vector startPoint = renderInfo.GetStartPoint();
                var y = startPoint[Vector.I2];
                var x = startPoint[Vector.I1];

                InsertChunk(new ImageChunk(imagePath, x, y));

                //m_FoundChunks.Add(new ImageChunk(imagePath, x, y));
            }
            catch (UnsupportedPdfException pdfException)
            {
            }
            catch (Exception argumentExceptionException)
            {
            }
        }

        public virtual void RenderText(TextRenderInfo renderInfo)
        {
            LineSegment segment = renderInfo.GetBaseline();
            if (renderInfo.GetRise() > m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset)
            {
                // remove the rise from the baseline - we do this because the text from a super/subscript render operations should probably be considered as part of the baseline of the text the super/sub is relative to
                Matrix riseOffsetTransform = new Matrix(0, -renderInfo.GetRise());
                segment = segment.TransformBy(riseOffsetTransform);
            }

            //This code assumes that if the baseline changes then we're on a newline
            Vector startPoint = renderInfo.GetBaseline().GetStartPoint();
            Vector topRight = renderInfo.GetAscentLine().GetEndPoint();
            iTextSharp.text.Rectangle rect = new iTextSharp.text.Rectangle(startPoint[Vector.I1], startPoint[Vector.I2], topRight[Vector.I1], topRight[Vector.I2]);
            int curFontSize = (int)Math.Round(Convert.ToDouble(rect.Height), 0, MidpointRounding.ToEven);

            TextChunk location = new TextChunk(
                renderInfo.GetText(),
                segment.GetStartPoint(),
                segment.GetEndPoint(),
                renderInfo.GetSingleSpaceWidth(),
                renderInfo.GetFont(),
                curFontSize);

            InsertChunk(location);

            //m_FoundChunks.Add(location);

            LogFont(location.Font.FullFontName[0][3], curFontSize);
        }

        protected bool IsChunkAtWordBoundary(TextChunk chunk, TextChunk previousChunk)
        {
            float dist = chunk.DistanceFromEndOf(previousChunk);
            if (dist < -chunk.CharSpaceWidth || dist > chunk.CharSpaceWidth / 2.0f)
                return true;

            return false;
        }

        private static int AscendingXpos(IPdfElementChunk chunk1, IPdfElementChunk chunk2)
        {
            return chunk1.XPos.CompareTo(chunk2.XPos);
        }

        private static float CalculateAverageChunkDistanceInLine(TextChunk[] lineChunks)
        {
            List<float> distances = new List<float>();
            for (int i = 1; i < lineChunks.Count(); i++)
            {
                distances.Add(lineChunks[i].XPos - lineChunks[i - 1].XPos - lineChunks[i - 1].Width);
            }

            if (distances.Count > 0)
            {
                return distances.Average();
            }
            return float.NaN;
        }

        private bool ChunkDistanceLooksLikeTable(TextChunk nextChunk, TextChunk thisChunk)
        {
            return ((nextChunk.XPos - (thisChunk.XPos + thisChunk.Width)) >
                    m_ProjectContext.ProjectConfiguration.MinTableCellSize);
        }

        private static void InsertColumnCandidate(Dictionary<float, int> columns, float xPos)
        {
            if (!columns.ContainsKey(xPos))
            {
                columns[xPos] = 1;
            }
            else
            {
                columns[xPos]++;
            }
        }

        private void AppendTextToCurrentBlock(TextChunk textChunk)
        {
            m_ContentBlock.Append(ProcessTextChunk(textChunk));
            m_ContentBlock.LastChunkBaseLine = textChunk.YPos;
        }

        private void CalculatePossibleColumnFromTextChunks()
        {
            m_PossibleColumns = new Dictionary<float, int>();
            IEnumerable<float> lines = m_FoundChunks.Select(chunk => chunk.YPos).Distinct();
            foreach (float line in lines)
            {
                TextChunk[] lineChunks = GetTextChunksInLine(line);

                if (!lineChunks.Any())
                    continue;

                float minXpos = lineChunks.Min(chunk => chunk.XPos);

                InsertColumnCandidate(m_PossibleColumns, minXpos);

                float averageChunkDistanceInLine = CalculateAverageChunkDistanceInLine(lineChunks);
                if (float.IsNaN(averageChunkDistanceInLine))
                    continue;

                for (int i = 1; i < lineChunks.Count(); i++)
                {
                    if ((lineChunks[i].XPos - lineChunks[i - 1].XPos - lineChunks[i - 1].Width) > 4 * averageChunkDistanceInLine)
                    {
                        InsertColumnCandidate(m_PossibleColumns, lineChunks[i].XPos);
                    }
                }
            }
        }

        private void CreateNewTableBlock(IEnumerable<TableRange> detectedTables, float currentLine)
        {
            TableRange matchingTableRange = detectedTables.FirstOrDefault(table => table.IsInTable(currentLine));
            if (matchingTableRange == null)
                return;

            m_ContentBlock = new ContentBlock(m_ProjectContext, ContentType.Table);

            for (int r = 0; r < matchingTableRange.TableRows.Count; r++)
            {
                ContentBlock rowBlock = new ContentBlock(m_ProjectContext, ContentType.TableRow);

                for (int c = 0; c < matchingTableRange.TableColums.Count; c++)
                {
                    rowBlock.SubBlocks.Add(new ContentBlock(m_ProjectContext, ContentType.TableCell));
                }

                m_ContentBlock.SubBlocks.Add(rowBlock);
            }
        }

        private void CreateNewTextBlock(TextChunk textChunk)
        {
            FontUsage fontUsage = GetFontUsage(textChunk.Font, textChunk.FontSize);
            m_ContentBlock = new ContentBlock(m_ProjectContext, ContentType.Text, fontUsage)
                                 {
                                     FirstChunkBaseLine = textChunk.YPos,
                                     OriginalXPosition = textChunk.XPos,
                                 };
        }

        private float DetectLastTableRow(TableRange range, float[] lines, float yPos)
        {
            int idx = -1;
            for (int i = 0; i < lines.Length; i++)
            {
                if (Math.Abs(lines[i] - yPos) < m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset)
                {
                    idx = i;
                    break;
                }
            }

            for (int i = idx - 1; i >= 0; i--)
            {
                float line = lines[i];
                TextChunk firstChunkInLine = GetTextChunksInLine(line).FirstOrDefault();
                if (firstChunkInLine != null)
                {
                    if (range.IsInColumn(0, firstChunkInLine))
                        return firstChunkInLine.YPos;
                    if (line > range.Start)
                    {
                        return line;
                    }
                }
            }
            return float.NaN;
        }

        private List<float> DetectParagraphs(float[] lines)
        {
            Dictionary<float, float> lineDistances = new Dictionary<float, float>();
            for (int i = 0; i < lines.Count(); i++)
            {
                lineDistances[lines[i]] = 0;
                if (i > 0)
                {
                    lineDistances[lines[i]] = lines[i - 1] - lines[i];
                }
            }

            return lines.Where(line => lineDistances[line] >= m_ProjectContext.ProjectConfiguration.ParagraphDistance).ToList();
        }

        private List<TableRange> DetectTablesOnPage(float[] lines)
        {
            List<TableRange> detectedTables = new List<TableRange>();
            TableRange range = new TableRange(m_ProjectContext);

            float firstRowOfTable = float.NaN;

            //foreach (var line in lines)
            for (int l = 0; l < lines.Length; l++)
            {
                float line = lines[l];

                TextChunk[] chunksInLine = GetChunksForLine(line).Where(chunk => chunk is TextChunk).Cast<TextChunk>().ToArray();

                bool endOfTableDetected = true;
                for (int i = 0; i < chunksInLine.Length - 1; i++)
                {
                    TextChunk thisChunk = chunksInLine[i];
                    TextChunk nextChunk = chunksInLine[i + 1];

                    if (ChunkDistanceLooksLikeTable(nextChunk, thisChunk))// ||
                        //IsInTableRange(range, thisChunk.XPos))
                    {
                        endOfTableDetected = false;
                        range.SetFirstLine(line + thisChunk.Height);

                        if (float.IsNaN(firstRowOfTable))
                        {
                            firstRowOfTable = line;
                        }

                        if (Math.Abs(line - firstRowOfTable) < m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset)
                        {
                            range.TableColums.InsertIfNew(nextChunk.XPos);
                        }
                    }
                }

                if ((Math.Abs(line - firstRowOfTable) < m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset) && (range.TableColums.Count > 0))
                {
                    range.TableColums.InsertIfNew(chunksInLine[0].XPos);

                    if (l < lines.Length - 1)
                    {
                        float nextLine = lines[l + 1];
                        TextChunk[] nextLineChunks = GetChunksForLine(nextLine)
                                .Where(chunk => chunk is TextChunk)
                                .Cast<TextChunk>()
                                .ToArray();

                        bool tableDetected = false;
                        for (int i = 0; i < nextLineChunks.Length - 1; i++)
                        {
                            TextChunk thisChunk = nextLineChunks[i];
                            TextChunk nextChunk = nextLineChunks[i + 1];

                            if (ChunkDistanceLooksLikeTable(nextChunk, thisChunk) ||
                                IsInTableRange(range, thisChunk.XPos))
                            {
                                tableDetected = true;
                                break;
                            }
                        }

                        if ((tableDetected) && nextLineChunks.Any())
                        {
                            range.TableColums.InsertIfNew(nextLineChunks[0].XPos);
                        }
                    }
                }

                if ((chunksInLine.Length > 0) && (range.IsInColumn(0, chunksInLine[0])))
                {
                    float lastRowPosition = DetectLastTableRow(range, lines, chunksInLine[0].YPos);
                    if ((float.IsNaN(lastRowPosition) || ((lastRowPosition - chunksInLine[0].YPos) > m_ProjectContext.ProjectConfiguration.TableDetectedMinRowDistance)))
                    {
                        range.TableRows.InsertIfNew(chunksInLine[0].YPos);
                    }
                }

                if ((chunksInLine.Length == 1) && IsInTableRange(range, chunksInLine[0].XPos))
                {
                    endOfTableDetected = false;
                }

                if (endOfTableDetected && (l > 0))
                {
                    range.SetLastLine(lines[l - 1]);
                }
                else if ((l == (lines.Count() - 1)) && (l > 0))
                {
                    range.SetLastLine(lines[l]);
                }

                if (range.Valid)
                {
                    detectedTables.Add(range);
                    range = new TableRange(m_ProjectContext);
                }
            }
            return detectedTables;
        }

        private bool EndsWithSpace(String str)
        {
            if (str.Length == 0) return false;
            return str[str.Length - 1] == ' ';
        }

        private int FindColumnIndex(IPdfElementChunk chunk, TableRange matchingTable)
        {
            int colIndex = -1;
            for (int i = 0; i < matchingTable.TableColums.Count; i++)
            {
                if (i < matchingTable.TableColums.Count - 1)
                {
                    if ((matchingTable.TableColums[i] - m_ProjectContext.ProjectConfiguration.TabeDetectedCellFuzzyness <= chunk.MidLineVertical)
                        && (chunk.MidLineVertical < matchingTable.TableColums[i + 1] + m_ProjectContext.ProjectConfiguration.TabeDetectedCellFuzzyness))
                    {
                        colIndex = i;
                        break;
                    }
                }
                else
                {
                    if (matchingTable.TableColums[i] - m_ProjectContext.ProjectConfiguration.TabeDetectedCellFuzzyness <= chunk.MidLineVertical)
                    {
                        colIndex = i;
                        break;
                    }
                }
            }

            if ((colIndex == -1) && (matchingTable.TableColums.Count > 0))
            {
                if (chunk.MidLineVertical < matchingTable.TableColums[0])
                {
                    colIndex = 0;
                }
                else if (chunk.MidLineVertical > matchingTable.TableColums[matchingTable.TableColums.Count - 1])
                {
                    colIndex = matchingTable.TableColums.Count - 1;
                }
            }
            return colIndex;
        }

        private int FindRowIndex(IPdfElementChunk chunk, TableRange matchingTable)
        {
            for (int i = 0; i < matchingTable.TableRows.Count; i++)
            {
                if ((i==0) && (matchingTable.Start >= chunk.MidLineHorizontal) && (matchingTable.TableRows.Count > 0) && (chunk.MidLineHorizontal > matchingTable.TableRows[0]))
                {
                    return 0;
                }
                
                if (i > 0)
                {
                    if ((matchingTable.TableRows[i - 1] + m_ProjectContext.ProjectConfiguration.TabeDetectedCellFuzzyness >= chunk.MidLineHorizontal)
                        && (chunk.MidLineHorizontal > matchingTable.TableRows[i] - m_ProjectContext.ProjectConfiguration.TabeDetectedCellFuzzyness))
                    {
                        return i;
                    }
                }
            }
            return -1;
        }

        private ContentBlock FindTableCellForChunk(IEnumerable<TableRange> detectedTables, ContentBlock contentBlock, IPdfElementChunk chunk)
        {
            TableRange matchingTable = detectedTables.FirstOrDefault(table => table.IsInTable(chunk.YPos));
            if (matchingTable == null)
                return null;

            var rowIndex = FindRowIndex(chunk, matchingTable);
            if (rowIndex == -1)
                return null;

            ContentBlock rowBlock = contentBlock.SubBlocks[rowIndex];

            int colIndex = FindColumnIndex(chunk, matchingTable);
            if (colIndex == -1)
                return null;

            return rowBlock.SubBlocks[colIndex];
        }

        private bool FontSizeIsInSelectedRange(TextChunk chunk)
        {
            if (m_ProjectContext.ProjectConfiguration == null)
                return true;

            if (chunk.FontSize < m_ProjectContext.ProjectConfiguration.MinFontSize)
                return false;

            return ((m_ProjectContext.ProjectConfiguration.MaxFontSize > 0) &&
                (chunk.FontSize <= m_ProjectContext.ProjectConfiguration.MaxFontSize)) || (Math.Abs(m_ProjectContext.ProjectConfiguration.MaxFontSize - 0) < m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset);
        }

        private IPdfElementChunk[] GetChunksForLine(float currentLine)
        {
            List<IPdfElementChunk> chunksOfLine = m_FoundChunks.Where(chunk => Math.Abs(chunk.YPos - currentLine) <
                m_ProjectContext.ProjectConfiguration.MaxOffsetForLine).ToList();
            chunksOfLine.Sort(AscendingXpos);
            return chunksOfLine.ToArray();
        }

        private FontUsage GetFontUsage(DocumentFont font, int fontSize)
        {
            return m_FontUsages.Get(font.FullFontName[0][3], fontSize);
        }

        private float[] GetLines()
        {
            var lines = m_FoundChunks.Select(chunk => chunk.YPos).Distinct().ToList();
            lines.Sort(SortDescendingLines);

            List<float> filteredLines = new List<float>();
            int l = 0;
            while (l < lines.Count)
            {
                if ((filteredLines.Count > 1) && (Math.Abs(lines[l] - filteredLines[filteredLines.Count - 1]) < 1))
                {
                    lines[filteredLines.Count - 1] = lines[l];
                }
                else
                {
                    filteredLines.Add(lines[l]);
                }
                l++;
            }

            return filteredLines.ToArray();
        }

        private TextChunk[] GetTextChunksInLine(float yPos)
        {
            return GetChunksForLine(yPos).Where(chunk => chunk is TextChunk).Cast<TextChunk>().ToArray();
        }

        private void InsertChunk(IPdfElementChunk chunk)
        {
            m_FoundChunks.Add(chunk);
        }

        private void InsertContentBlock(ContentBlock contentBlock)
        {
            if (contentBlock == null)
                return;

            //if (contentBlock.ContentType == ContentType.Text)
            //{
            //    if (contentBlock.Replacement.Tag == HtmlReplacementTag.H1)
            //    {
            //        string chapterHeader = contentBlock.Content.Trim();
            //        m_CurrentChapter = new Chapter(chapterHeader);
            //    }
            //}

            m_ContentBlocks.Add(contentBlock);
        }

        private bool IsCmykImage(PdfImageObject pdfImageObject)
        {
            PdfName colorSpace = pdfImageObject.GetDictionary().Get(PdfName.COLORSPACE) as PdfName;
            return Equals(colorSpace, PdfName.DEVICECMYK);
        }

        private bool IsFirstTextChunkInLine(TextChunk textChunk, IPdfElementChunk[] chunksInCurrentLine)
        {
            if (chunksInCurrentLine.Length == 0)
                return false;

            return (chunksInCurrentLine[0] == textChunk);
        }

        private bool IsIndent(IPdfElementChunk pdfChunk, IPdfElementChunk[] chunksInCurrentLine)
        {
            TextChunk textChunk = pdfChunk as TextChunk;
            if ((textChunk == null) || (m_LastChunk == null))
                return false;

            if (IsFirstTextChunkInLine(textChunk, chunksInCurrentLine) && IsInLineStartRange(textChunk))
            {
                var previousLineStartChunk = m_FoundChunks.Where(chunk => chunk is TextChunk)
                    .Cast<TextChunk>().FirstOrDefault(chunk => Math.Abs(chunk.YPos - m_LastChunk.YPos) < m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset);

                return ((previousLineStartChunk != null) &&
                        (textChunk.XPos > previousLineStartChunk.XPos + m_ProjectContext.ProjectConfiguration.IndentDistance));
            }
            return false;
        }

        private bool IsInLineStartRange(TextChunk textChunk)
        {
            float avgLineStart = GetAverageFirstXPosInLine;
            float rangeStart = (float)(avgLineStart - (0.25 * avgLineStart));
            float rangeEnd = (float)(avgLineStart + (0.25 * avgLineStart));
            bool isInLineStartRange = (rangeStart <= textChunk.XPos) && (textChunk.XPos <= rangeEnd);
            return isInLineStartRange;
        }

        private bool IsInTable(IEnumerable<TableRange> detectedTables, float line)
        {
            return detectedTables.Any(table => table.IsInTable(line));
        }

        private bool IsInTableRange(TableRange range, float xPos)
        {
            if (!range.TableColums.Any())
                return false;
            return xPos > range.TableColums.Min();
        }

        private bool IsNewParagraph(List<float> paragraphStartLines, float yPos)
        {
            if (paragraphStartLines.Contains(yPos))
            {
                paragraphStartLines.Remove(yPos);
                return true;
            }
            return false;
        }

        private void LogFont(string fontName, int fontSize)
        {
            if (m_FontUsages.Contains(fontName, fontSize))
            {
                m_FontUsages.Get(fontName, fontSize).UsageCount++;
            }
            else
            {
                m_FontUsages.Add(new FontUsage(fontName, fontSize));
            }
        }

        private void LogLineDistance(float distance)
        {
            m_LineDistances.Add(distance);
        }

        private bool NewContentBlockRequired(IPdfElementChunk chunk)
        {
            if ((chunk == null) || (m_LastChunk == null))
                return true;

            if (chunk is ImageChunk)
            {
                return true;
            }

            if (chunk.GetType() != m_LastChunk.GetType())
            {
                return true;
            }

            TextChunk textChunk = chunk as TextChunk;
            TextChunk lastTextChunk = m_LastChunk as TextChunk;

            if ((textChunk == null) || (lastTextChunk == null))
                return true;

            FontUsage textChunkFont = GetFontUsage(textChunk.Font, textChunk.FontSize);
            FontUsage lastTextChunkFont = GetFontUsage(lastTextChunk.Font, lastTextChunk.FontSize);

            string textChunReplacement = string.Empty;
            string lastTextChunReplacement = string.Empty;

            if (m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection.Contains(textChunkFont))
            {
                textChunReplacement = m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection[textChunkFont].HtmlReplacementStyle;
            }

            if (m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection.Contains(lastTextChunkFont))
            {
                lastTextChunReplacement = m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection[lastTextChunkFont].HtmlReplacementStyle;
            }

            if ((!string.IsNullOrEmpty(textChunReplacement)) && (!string.IsNullOrEmpty(lastTextChunReplacement)))
            {
                if ((textChunReplacement != ".none") || (lastTextChunReplacement != ".none"))
                {
                    return (textChunReplacement != lastTextChunReplacement);
                }
            }
            return (textChunkFont.CompareTo(lastTextChunkFont) != 0);
        }

        private bool PositionIsInSelectedRange(TextChunk chunk)
        {
            if (chunk == null)
                return false;

            if (m_ProjectContext.ProjectConfiguration == null)
                return true;

            if ((m_ProjectContext.ProjectConfiguration.StartHeight == 0.0) && (m_ProjectContext.ProjectConfiguration.EndHeight == 0.0))
                return true;

            float positionY = chunk.startLocation[1];
            return ((positionY >= m_ProjectContext.ProjectConfiguration.StartHeight) && (positionY <= m_ProjectContext.ProjectConfiguration.EndHeight));
        }

        private ContentBlock ProcessImageChunk(ImageChunk imageChunk)
        {
            ContentBlock block = new ContentBlock(m_ProjectContext, ContentType.Image, Path.GetFileName(imageChunk.ImageFilePath)) { FirstChunkBaseLine = imageChunk.YPos, OriginalXPosition = imageChunk.XPos };
            return block;
        }

        private string ProcessTextChunk(TextChunk chunk)
        {
            TextChunk lastChunk = m_LastChunk as TextChunk;

            StringBuilder sb = new StringBuilder();
            if (lastChunk == null)
            {
                PutText(sb, chunk);
            }
            else
            {
                if (chunk.SameLine(lastChunk))
                {
                    // we only insert a blank space if the trailing character of the previous string wasn't a space, and the leading character of the current string isn't a space
                    if (IsChunkAtWordBoundary(chunk, lastChunk) && !StartsWithSpace(chunk.text) &&
                        !EndsWithSpace(lastChunk.text))
                    {
                        //if (Math.Abs(chunk.XPos - lastChunk.distParallelEnd) > (chunk.CharSpaceWidth/2))
                        if ((chunk.XPos - lastChunk.distParallelEnd) > (chunk.CharSpaceWidth / 2))
                        {
                            PutText(sb, " ");
                        }
                    }
                        
                    PutText(sb, chunk);
                }
                else
                {
                    PutText(sb, "\n");
                    PutText(sb, chunk);

                    float distance = lastChunk.YPos - chunk.YPos;
                    LogLineDistance(distance);
                }
            }
            return sb.ToString();
        }

        private void PutText(StringBuilder sb, TextChunk chunk)
        {
            sb.Append(ReplaceCharacters(chunk.text));
        }

        private void PutText(StringBuilder sb, string txt)
        {
            sb.Append(txt);
        }

        private void RemoveChunksWithRemoveStyle()
        {
            List<IPdfElementChunk> removeList = new List<IPdfElementChunk>();
            foreach (var chunk in m_FoundChunks)
            {
                TextChunk textChunk = chunk as TextChunk;
                if (textChunk == null)
                    continue;

                FontUsage fontUsage = new FontUsage(textChunk.Font.FullFontName[0][3], textChunk.FontSize);
                FontAssignmentData fontAssignmentData = m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection[fontUsage];
                if ((fontAssignmentData != null) && (fontAssignmentData.HtmlReplacementStyle != null))
                {
                    HtmlReplacement htmlReplacement = m_ProjectContext.ProjectConfiguration.HtmlReplacements[fontAssignmentData.HtmlReplacementStyle];
                    if ((htmlReplacement != null) && (htmlReplacement.Tag == HtmlReplacementTag.Remove))
                    {
                        removeList.Add(chunk);
                    }
                }
            }

            foreach (IPdfElementChunk chunk in removeList)
            {
                m_FoundChunks.Remove(chunk);
            }
        }

        private void RemoveTextchunksOutsideDetectionRange()
        {
            if (m_ProjectContext.ProjectConfiguration.StartHeight > 0)
            {
                IPdfElementChunk[] chunksToRemove =
                    m_FoundChunks.Where(chunk => chunk.YPos < m_ProjectContext.ProjectConfiguration.StartHeight).ToArray();
                foreach (IPdfElementChunk chunk in chunksToRemove)
                {
                    if (chunk is TextChunk)
                    {
                        m_FoundChunks.Remove(chunk);
                    }
                }
            }

            if (m_ProjectContext.ProjectConfiguration.EndHeight > 0)
            {
                IPdfElementChunk[] chunksToRemove =
                    m_FoundChunks.Where(chunk => chunk.YPos > m_ProjectContext.ProjectConfiguration.EndHeight).ToArray();
                foreach (IPdfElementChunk chunk in chunksToRemove)
                {
                    m_FoundChunks.Remove(chunk);
                }
            }
        }

        private string ReplaceCharacters(string text)
        {
            if (string.IsNullOrEmpty(text))
                return text;

            foreach (TextReplacement replacement in m_ProjectContext.ProjectConfiguration.TextReplacements)
            {
                text = text.Replace(replacement.OriginalText, replacement.ReplacementText);
            }
            return text;
        }

        private Image ResizeImage(Image image, int longSize, bool shrinkOnly)
        {
            var ratioX = (double)longSize / image.Width;
            var ratioY = (double)longSize / image.Height;
            var ratio = Math.Min(ratioX, ratioY);

            if ((ratio >= 1) && (shrinkOnly))
            {
                return image;
            }

            var newWidth = (int)(image.Width * ratio);
            var newHeight = (int)(image.Height * ratio);

            var newImage = new Bitmap(newWidth, newHeight);
            Graphics.FromImage(newImage).DrawImage(image, 0, 0, newWidth, newHeight);
            return newImage;
        }

        private int SortDescendingLines(float x, float y)
        {
            return -1 * x.CompareTo(y);
        }

        private int SortPdfChunks(IPdfElementChunk chunk1, IPdfElementChunk chunk2)
        {
            if (Math.Abs(chunk1.YPos - chunk2.YPos) < m_ProjectContext.ProjectConfiguration.TableDetectionAllowedBaselineOffset)
            {
                return chunk1.XPos.CompareTo(chunk2.XPos);
            }
            return chunk2.YPos.CompareTo(chunk1.YPos) * 1000000;
        }

        private bool StartsWithSpace(String str)
        {
            if (str.Length == 0) return false;
            return str[0] == ' ';
        }
    }

    public class TableGridList : List<float>
    {
        private readonly bool m_SortDescending;

        public TableGridList(bool sortDescending)
        {
            m_SortDescending = sortDescending;
        }

        public void InsertIfNew(float columnPosition)
        {
            if (Contains(columnPosition))
                return;

            if ((Count > 0) && (columnPosition < this.Max()) && (columnPosition > this.Min()))
                return;

            Add(columnPosition);

            if (m_SortDescending)
            {
                Sort(SortDescending);
            }
            else
            {
                Sort();
            }
        }

        private int SortDescending(float x, float y)
        {
            return -1 * x.CompareTo(y);
        }
    }

    public class TableRange
    {
        private IProjectContext m_ProjectContext;

        public TableRange(IProjectContext context)
        {
            m_ProjectContext = context;
            Start = float.NaN;
            End = float.NaN;
            TableColums = new TableGridList(false);
            TableRows = new TableGridList(true);
        }

        public float End { get; private set; }

        public float Start { get; private set; }

        public TableGridList TableColums { get; private set; }

        public TableGridList TableRows { get; private set; }

        public bool Valid
        {
            get { return ((!float.IsNaN(Start)) && (!float.IsNaN(End))) && (Math.Abs(Start - End) > 0); }
        }

        public bool IsInColumn(int columnNr, TextChunk thisChunk)
        {
            if (TableColums.Count > columnNr + 1)
            {
                return ((thisChunk.XPos >= TableColums[columnNr]) && (thisChunk.XPos <= TableColums[columnNr + 1]));
            }
            if (columnNr == TableColums.Count - 1)
            {
                return thisChunk.XPos >= TableColums[columnNr];
            }
            return false;
        }

        public bool IsInTable(float line)
        {
            return ((Start + m_ProjectContext.ProjectConfiguration.TabeDetectedCellFuzzyness >= line) && (line >= End - m_ProjectContext.ProjectConfiguration.TabeDetectedCellFuzzyness));
        }

        public void SetFirstLine(float line)
        {
            if (float.IsNaN(Start))
            {
                Start = line;
            }
        }

        public void SetLastLine(float line)
        {
            if (float.IsNaN(Start))
            {
                return;
            }

            if (float.IsNaN(End))
            {
                End = line;
            }
        }

        public override string ToString()
        {
            return string.Format("TableRange {0} - {1} ({2} columns, {3} rows", Start, End, TableColums.Count, TableRows.Count);
        }
    }
}